/*  apr_dbd_odbc.c      APR DBD ODBC driver  March, 2007    for APR 1.2.8
Copyright 2007 Tom Donovan

Licensed under the Apache License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. 
You may obtain a copy of the License at 

    http://www.apache.org/licenses/LICENSE-2.0 

Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions and 
limitations under the License.

*/

#include "apr.h"
#include "apr_version.h"
#include "apr_strings.h"
#include "apr_buckets.h"
#include "apr_env.h"
#include "apr_file_io.h"
#include "apr_file_info.h"
#include "apr_dbd_internal.h"
#include "apr_thread_proc.h"

#include <sql.h>
#include <sqlext.h>

static SQLHANDLE henv = NULL;           /* ODBC ENV handle is process-wide */
static apr_threadkey_t *odbc_tlskey;    /* for messages when there is no dbc */

/* Use a CHECK_ERROR macro so we can grab the source line numbers
 * for error reports */
static void check_error(apr_dbd_t *a, const char *step, SQLRETURN rc,
                 SQLSMALLINT type, SQLHANDLE h, int line);
#define CHECK_ERROR(a,s,r,t,h)  check_error(a,s,r,t,h, __LINE__)

#define SOURCE_FILE "apr_dbd_odbc.c"    /* source file for error messages */
#define MAX_ERROR_STRING 1024           /* max length of message in dbc */
#define MAX_COLUMN_NAME 256             /* longest column name recognized */
#define DEFAULT_BUFFER_SIZE 1024        /* value for defaultBufferSize */
#define TEXTMODE 1

#define MAX_PARAMS  20
#define DEFAULTSEPS " \t\r\n,="
#define CSINGLEQUOTE '\''
#define SSINGLEQUOTE "\'"


/* Convert a SQL result to an APR result */
#define APR_FROM_SQL_RESULT(rc) \
    (SQL_SUCCEEDED(rc) ? APR_SUCCESS : APR_EGENERAL)

/* DBD opaque structures */
struct apr_dbd_t {
    SQLHANDLE dbc;          /* SQL connection handle */
    apr_pool_t *pool;       /* connection lifetime pool */
    char *dbname;           /* ODBC datasource */
    int lasterrorcode;
    int lineNumber;
    char lastError[MAX_ERROR_STRING];
    int defaultBufferSize;
    int transaction_mode;
    int dboptions;          /* options about SQLGetData abilities */
    int default_transaction_mode;
    int can_commit;
    apr_dbd_results_t *results; /* NULL unless active results to be closed */
};

struct apr_dbd_results_t {
    SQLHANDLE stmt;         /* parent statement handle */
    SQLHANDLE dbc;          /* parent connection handle */
    char **colnames;        /* array of column names (NULL until requested) */
    apr_pool_t *pool;       /* pool from query or select */
    apr_dbd_t *apr_dbd;     /* parent DBD connection handle */
    int random;             /* random access requested */
    int ncols;              /* number of columns */
    SQLPOINTER *colptrs;        /* pointers to column data */
    SQLINTEGER *colsizes;       /* max-sizes (before fetch - actual after) */
    SQLINTEGER *coltextsizes;   /* max-sizes if converted to text */
    SQLSMALLINT *coltypes;      /* array of SQL data types for columns */
    int *all_data_fetched;  /* flags data as all fetched, for LOBs  */
    void *data;             /* buffer for all data for one row */

};

struct apr_dbd_row_t {
    SQLHANDLE stmt;         /* parent ODBC statement handle */
    SQLHANDLE dbc;          /* parent ODBC connection handle */
    apr_pool_t *pool;       /* pool from get_row */
    apr_dbd_results_t *res;
};

struct apr_dbd_transaction_t {
    SQLHANDLE dbc;          /* parent ODBC connection handle */
    apr_dbd_t *apr_dbd;     /* parent DBD connection handle */
};

struct apr_dbd_prepared_t {
    SQLHANDLE stmt;         /* ODBC statement handle */
    SQLHANDLE dbc;          /* parent ODBC connection handle */
    apr_dbd_t *apr_dbd;
    int nargs;
    int nvals;
    int *types;             /* array of DBD data types */

};
/*
*   local functions
*/

/* close any open results for the connection */
static apr_status_t odbc_close_results(void *d)
{   apr_dbd_t *dbc = (apr_dbd_t *) d;
    SQLRETURN rc = SQL_SUCCESS;
    
    if (dbc->results) {
        rc = SQLCloseCursor(dbc->results->stmt);
		        dbc->results = NULL;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* close the ODBC statement handle from a select or prepare */
static apr_status_t odbc_close_stmt(apr_dbd_prepared_t *statement)
{   
    SQLRETURN rc = APR_SUCCESS;
    SQLHANDLE hstmt = statement->stmt;
    /* stmt is closed if connection has already been closed */
    if (hstmt && statement->apr_dbd && statement->apr_dbd->dbc) {
        rc = SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        }
    statement->stmt = NULL;
    return APR_FROM_SQL_RESULT(rc);
}

/** close: close/release a connection obtained from open() **/
static apr_status_t odbc_close(apr_dbd_t *handle)
{
    SQLRETURN rc = SQL_SUCCESS;

    if (handle->dbc) {
        rc = SQLDisconnect(handle->dbc);
        CHECK_ERROR(handle, "SQLDisconnect", rc, SQL_HANDLE_DBC, handle->dbc);
        rc = SQLFreeHandle(SQL_HANDLE_DBC, handle->dbc);
        CHECK_ERROR(handle, "SQLFreeHandle (DBC)", rc, SQL_HANDLE_ENV, henv);
        handle->dbc = NULL;
    }
    return APR_FROM_SQL_RESULT(rc);
}

/* odbc_close re-defined for passing to pool cleanup */
static apr_status_t odbc_close_cleanup(void *handle)
{
    return odbc_close( (apr_dbd_t *) handle);
}

/* close the ODBC environment handle at process termination */
static apr_status_t odbc_close_env(SQLHANDLE henv)
{   
    SQLRETURN rc;

    rc = SQLFreeHandle(SQL_HANDLE_ENV, henv);
    return APR_FROM_SQL_RESULT(rc);
}


static int odbc_get_sizes_types(apr_dbd_results_t *res, SQLHANDLE stmt,
                         SQLSMALLINT cols, SQLINTEGER *sizes,
                         SQLINTEGER *textsizes, SQLSMALLINT *types)
{
    SQLRETURN rc = SQL_SUCCESS;
    int c, maxsize, tsize = 0;

    /* Its remarkable what info an ODBC driver is allowed not to provide,
     * so we must default for almost everything */
    for (c=0 ; c < cols ; c++) {
        /* type */
        rc = SQLColAttribute(stmt, c+1, SQL_DESC_TYPE, NULL, 0, NULL,
                             (SQLPOINTER) &types[c]);
        /* many ODBC v2 datasources need this 2nd try */
        if (!SQL_SUCCEEDED(rc))
            rc = SQLColAttribute(stmt, c+1, SQL_DESC_CONCISE_TYPE, NULL, 0,
                                 NULL, (SQLPOINTER) &types[c]);
        CHECK_ERROR(res->apr_dbd, "SQLColAttribute (SQL_DESC_TYPE)", rc,
                    SQL_HANDLE_STMT, stmt);
        /* unknown datatype is always CHAR */
        if (!SQL_SUCCEEDED(rc))
            types[c] = SQL_C_CHAR;

        /* size if retrieved as text */
        rc = SQLColAttribute(stmt, c+1, SQL_DESC_DISPLAY_SIZE, NULL, 0, NULL,
                             (SQLPOINTER) &textsizes[c]);
        CHECK_ERROR(res->apr_dbd, "SQLColAttribute (SQL_DESC_DISPLAY_SIZE)",
                    rc, SQL_HANDLE_STMT, stmt);
        /* Memo field returns size = 2gb, type = -1 */
        if (!SQL_SUCCEEDED(rc))
            textsizes[c] = 0;
        /* add 1 or 2 bytes to allow for null term char on strings,
         * but keep aligned to prevent odd addresses for efficiency */
        else if (textsizes[c] < 0) textsizes[c] = 0;
        else textsizes[c] += (2 - (textsizes[c] & 1));
        /* real size */
        rc = SQLColAttribute(stmt, c+1, SQL_DESC_OCTET_LENGTH, NULL, 0,
                             NULL, (SQLPOINTER) &sizes[c]);
        CHECK_ERROR(res->apr_dbd, "SQLColAttribute (SQL_DESC_OCTET_LENGTH)",
                    rc, SQL_HANDLE_STMT, stmt);
        if (!SQL_SUCCEEDED(rc))
            sizes[c] = textsizes[c];

        maxsize = (textsizes[c] > sizes[c]) ? textsizes[c] : sizes[c];
        /* LONG types are never bound for their real (1-2gb) length */
        if (   types[c] == SQL_LONGVARCHAR
            || types[c] == SQL_LONGVARBINARY
            || maxsize <= 0 ) {
            maxsize = res->apr_dbd->defaultBufferSize;
        }
        tsize += maxsize;
    }

    return SQL_SUCCEEDED(rc) ? tsize : 0;
}

/* bind a result column (1-based) to an address, length, & size.
 * Sets size to receive the actual length at SQLFetch call */
static SQLRETURN odbc_bind_column(apr_dbd_t *apr_dbc, SQLHANDLE stmt,
                                  SQLSMALLINT col, void *ptr, SQLSMALLINT type,
                                  SQLSMALLINT *bound_type, SQLINTEGER size,
                                  SQLINTEGER textsize, SQLINTEGER *actual_size,
                           int textmode)
{
    SQLRETURN rc;
    SQLINTEGER bindsize = size;
    SQLSMALLINT bindtype = type;

    /* don't bind LOBs - they always use SQLGetData */
    if (type == SQL_LONGVARCHAR || type == SQL_LONGVARBINARY) {
        if (textmode) {
            *actual_size = apr_dbc->defaultBufferSize;
        }
        else {
            *actual_size = 0;
        }
        *bound_type = SQL_C_DEFAULT;
        return SQL_SUCCESS;
    }

    else if (textmode) {
        bindsize = textsize;
        bindtype = SQL_C_CHAR;
    }

    rc = SQLBindCol(stmt, col, bindtype, ptr, bindsize, actual_size);
    CHECK_ERROR(apr_dbc, "SQLBindCol", rc, SQL_HANDLE_STMT, stmt);
    *bound_type = bindtype;
    return rc;
}

/* build and populate an apr_dbd_results_t for a select */
static SQLRETURN odbc_bind_results(apr_dbd_t *handle, SQLHANDLE hstmt,
                            apr_pool_t *pool, int random, int textmode,
                            apr_dbd_results_t **res)
 {
    SQLRETURN rc;
    SQLSMALLINT ncols;

    *res = apr_pcalloc(pool, sizeof(apr_dbd_results_t));
    (*res)->stmt = hstmt;
    (*res)->dbc = handle->dbc;
    (*res)->pool = pool;
    (*res)->random = random;
    (*res)->apr_dbd = handle;
    rc = SQLNumResultCols(hstmt, &ncols);
    CHECK_ERROR(handle, "SQLNumResultCols", rc, SQL_HANDLE_STMT, hstmt);
    (*res)->ncols = ncols;
    handle->results = *res;

    if SQL_SUCCEEDED(rc) {
        char *ptr;
        int tlen, c;

        (*res)->colnames = apr_pcalloc(pool, ncols * sizeof(char *));
        (*res)->colptrs = apr_pcalloc(pool, ncols * sizeof(void *));
        (*res)->colsizes = apr_pcalloc(pool, ncols * sizeof(SQLINTEGER));
        (*res)->coltextsizes = apr_pcalloc(pool, ncols * sizeof(SQLINTEGER));
        (*res)->coltypes = apr_pcalloc(pool, ncols * sizeof(SQLSMALLINT));
        (*res)->all_data_fetched = apr_pcalloc(pool, ncols * sizeof(int));
        (*res)->ncols = ncols;

        /* bind columns and put size, type, and pointer arrays into results */
        tlen = odbc_get_sizes_types(*res, hstmt, ncols, (*res)->colsizes,
                                    (*res)->coltextsizes, (*res)->coltypes);
        (*res)->data = apr_pcalloc(pool, tlen);
        ptr = (*res)->data;
        for(c = 0 ; c < ncols && (!rc) ; c++) {
            (*res)->colptrs[c] = ptr;
            /* binary=0 will bind all columns as string or CLOB
             * instead of the real datatype */
            rc = odbc_bind_column(handle, hstmt, c+1, (void *) ptr,
                                  (*res)->coltypes[c], (*res)->coltypes+c,
                                  (*res)->colsizes[c], (*res)->coltextsizes[c],
                                  (*res)->colsizes + c,  TEXTMODE);
            ptr += (*res)->colsizes[c];
        }
    }
    return rc;
}

/* bind a parameter - input params only, does not support output parameters */
static SQLRETURN odbc_bind_param(apr_pool_t *pool,
                                 apr_dbd_prepared_t *statement, int narg,
                                 SQLSMALLINT type, int *argp,
                                 const char **args, int textmode)
{
    SQLRETURN rc;
    void *ptr;
    SQLUINTEGER len;

    ptr= (void *) args[*argp];
    len=(SQLUINTEGER) strlen(ptr);
	(*argp)++;								/* consume one arg */

    rc = SQLBindParameter(
            statement->stmt,                /* statement handle */
             narg,                          /* parameter number */
             SQL_PARAM_INPUT,               /* input or output parameter */
             SQL_C_CHAR,                     /* ODBC C type */
             SQL_CHAR,                      /* ODBC base type */
             len,                           /* buffer size */
             0,                             /* decimal digits */
             ptr,                           /* value pointer */
             len,                           /* buffer length */
             NULL);                         /* ptr to NULL indicator */
    CHECK_ERROR(statement->apr_dbd, "SQLBindParameter", rc, SQL_HANDLE_STMT, 
                statement->stmt);
    return rc;
}

/* Parse the parameter string for open */
static apr_status_t odbc_parse_params(apr_pool_t *pool, const char *params,
                               int *connect, SQLCHAR **datasource, SQLCHAR **user,
                               SQLCHAR **password, int *defaultBufferSize,
                               int *nattrs,int **attrs, int **attrvals)
{
    char *seps, *last, *name[MAX_PARAMS], *val[MAX_PARAMS];
    int nparams=0, i, j;

    *attrs = apr_pcalloc(pool, MAX_PARAMS * sizeof(char *));
    *attrvals = apr_pcalloc(pool, MAX_PARAMS * sizeof(int));
    *nattrs = 0;
    seps = DEFAULTSEPS;
    name[nparams] = apr_strtok(apr_pstrdup(pool, params), seps, &last);
    do {
        if (last[strspn(last, seps)] == CSINGLEQUOTE) {
            last += strspn(last, seps);
            seps=SSINGLEQUOTE;
        }
        val[nparams] = apr_strtok(NULL, seps, &last);
        seps = DEFAULTSEPS;
        name[++nparams] = apr_strtok(NULL, seps, &last);
    } while ( nparams <= MAX_PARAMS && name[nparams] != NULL
              && val[nparams] != NULL);

    for(j=i=0 ; i< nparams ; i++)
    {   if      (!apr_strnatcasecmp(name[i], "CONNECT"))
        {   *datasource = apr_pstrdup(pool, val[i]);
            *connect=1;
        }
        else if (!apr_strnatcasecmp(name[i], "DATASOURCE"))
        {   *datasource = apr_pstrdup(pool, val[i]);
            *connect=0;
        }
        else if (!apr_strnatcasecmp(name[i], "USER"))
            *user = apr_pstrdup(pool, val[i]);
        else if (!apr_strnatcasecmp(name[i], "PASSWORD"))
            *password = apr_pstrdup(pool, val[i]);
        else if (!apr_strnatcasecmp(name[i], "BUFSIZE"))
            *defaultBufferSize = atoi(val[i]);
        else if (!apr_strnatcasecmp(name[i], "ACCESS"))
        {   if  (!apr_strnatcasecmp(val[i], "READ_ONLY"))
                (*attrvals)[j] = SQL_MODE_READ_ONLY;
            else if (!apr_strnatcasecmp(val[i], "READ_WRITE"))
                (*attrvals)[j] = SQL_MODE_READ_WRITE;
            else return SQL_ERROR;
            (*attrs)[j++] = SQL_ATTR_ACCESS_MODE;
        }
        else if (!apr_strnatcasecmp(name[i], "CTIMEOUT"))
        {   (*attrvals)[j] = atoi(val[i]);
            (*attrs)[j++] = SQL_ATTR_LOGIN_TIMEOUT;
        }
        else if (!apr_strnatcasecmp(name[i], "STIMEOUT"))
        {   (*attrvals)[j] = atoi(val[i]);
            (*attrs)[j++] = SQL_ATTR_CONNECTION_TIMEOUT;
        }
        else if (!apr_strnatcasecmp(name[i], "TXMODE"))
        {   if      (!apr_strnatcasecmp(val[i], "READ_UNCOMMITTED"))
                (*attrvals)[j] = SQL_TXN_READ_UNCOMMITTED;
            else if (!apr_strnatcasecmp(val[i], "READ_COMMITTED"))
                (*attrvals)[j] = SQL_TXN_READ_COMMITTED;
            else if (!apr_strnatcasecmp(val[i], "REPEATABLE_READ"))
                (*attrvals)[j] = SQL_TXN_REPEATABLE_READ;
            else if (!apr_strnatcasecmp(val[i], "SERIALIZABLE"))
                (*attrvals)[j] = SQL_TXN_SERIALIZABLE;
            else if (!apr_strnatcasecmp(val[i], "DEFAULT"))
                continue;
            else return SQL_ERROR;
            (*attrs)[j++] = SQL_ATTR_TXN_ISOLATION;
        }
        else return SQL_ERROR;
    }
    *nattrs = j;
    return (*datasource && *defaultBufferSize) ? APR_SUCCESS : SQL_ERROR;
}

/* common handling after ODBC calls - save error info (code and text) in dbc,
which might be in TLS if there's no real dbc */
static void check_error(apr_dbd_t *dbc, const char *step, SQLRETURN rc,
                 SQLSMALLINT type, SQLHANDLE h, int line)
{
    SQLCHAR buffer[512];
    SQLCHAR sqlstate[128];
    SQLINTEGER native;
    SQLSMALLINT reslength;
    char *res, *p, *end, *logval=NULL;
    int i;
    apr_status_t r;

    /* set info about last error in dbc  - fast return for SQL_SUCCESS  */
    if (rc == SQL_SUCCESS) {
        char successMsg[] = "[dbd_odbc]SQL_SUCCESS ";
        dbc->lasterrorcode = SQL_SUCCESS;
        strcpy(dbc->lastError, successMsg);
        strcpy(dbc->lastError+sizeof(successMsg)-1, step);
        return;
    }
    switch (rc) {
        case SQL_INVALID_HANDLE     : { res = "SQL_INVALID_HANDLE"; break; }
        case SQL_ERROR              : { res = "SQL_ERROR"; break; }   
        case SQL_SUCCESS_WITH_INFO  : { res = "SQL_SUCCESS_WITH_INFO"; break; }
        case SQL_STILL_EXECUTING    : { res = "SQL_STILL_EXECUTING"; break; }
        case SQL_NEED_DATA          : { res = "SQL_NEED_DATA"; break; }
        case SQL_NO_DATA            : { res = "SQL_NO_DATA"; break; }
        default                     : { res = "unrecognized SQL return code"; }
    }
    /* these two returns are expected during normal execution */
    if (rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA)
        dbc->can_commit = 0;
    p = dbc->lastError;
    end = p + sizeof(dbc->lastError);
    dbc->lasterrorcode = rc;
    p += sprintf(p, "[dbd_odbc]%.64s returned %.30s (%d) at %.24s:%d ",
                 step, res, rc, SOURCE_FILE, line-1);
    for (i=1, rc=0 ; rc==0 ; i++) {
        rc = SQLGetDiagRec(type, h, i, sqlstate, &native, buffer, 
                            sizeof(buffer), &reslength);
        if (SQL_SUCCEEDED(rc) && (p < (end-280))) 
            p += sprintf(p, "%.256s %.20s ", buffer, sqlstate);
    }
    r = apr_env_get(&logval, "apr_dbd_odbc_log", dbc->pool);
    if (logval) {
        apr_file_t *se;
        apr_file_open_stderr(&se, dbc->pool);
        apr_file_printf(se, "%s\n", dbc->lastError);
    }
}

/*
*   public functions per DBD driver API
*/

/** init: allow driver to perform once-only initialisation. **/
static void odbc_init(apr_pool_t *pool)
{
    SQLRETURN rc;
    char *step;
    apr_dbd_t *tmp_dbc;
    apr_status_t r;

    apr_threadkey_private_create(&odbc_tlskey, NULL, pool);
    step = "SQLAllocHandle (SQL_HANDLE_ENV)";
    rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
    apr_pool_cleanup_register(pool, henv, odbc_close_env, odbc_close_env);
    if (SQL_SUCCEEDED(rc))
    {   step = "SQLSetEnvAttr";
        rc = SQLSetEnvAttr(henv,SQL_ATTR_ODBC_VERSION,
                          (SQLPOINTER) SQL_OV_ODBC3, 0);
    }
    /* no db handle created, so put message into thread local area */
    tmp_dbc = apr_pcalloc(pool, sizeof(apr_dbd_t));
    tmp_dbc->pool = pool;
     r = apr_threadkey_private_set(tmp_dbc, odbc_tlskey);
    CHECK_ERROR(tmp_dbc, step, rc, SQL_HANDLE_ENV, henv);
}

/** native_handle: return the native database handle of the underlying db **/
static void* odbc_native_handle(apr_dbd_t *handle)
{   return handle->dbc;
}

/** open: obtain a database connection from the server rec. **/

/* It would be more efficient to allocate a single statement handle 
    here - but SQL_ATTR_CURSOR_SCROLLABLE must be set before
    SQLPrepare, and we don't know whether random-access is
    specified until SQLExecute so we cannot.
*/

static apr_dbd_t* odbc_open(apr_pool_t *pool, const char *params)
{
    SQLRETURN   rc;
    SQLHANDLE   hdbc;
    apr_dbd_t   *handle;
    char *err_step;
    int err_htype, i;
    int defaultBufferSize=DEFAULT_BUFFER_SIZE;
    SQLHANDLE err_h;
    SQLCHAR  *datasource="", *user="", *password="";
    int nattrs=0, *attrs=NULL, *attrvals=NULL, connect=0;

    err_step="SQLAllocHandle (SQL_HANDLE_DBC)";
    err_htype = SQL_HANDLE_ENV;
    err_h = henv;
    rc = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (SQL_SUCCEEDED(rc)) {
        err_step="Invalid DBD Parameters - open";
        err_htype = SQL_HANDLE_DBC;
        err_h = hdbc;
        rc = odbc_parse_params(pool, params, &connect, &datasource, &user,
                               &password, &defaultBufferSize, &nattrs, &attrs,
                               &attrvals);
    }
    if (SQL_SUCCEEDED(rc)) {
        for (i=0 ; i < nattrs && SQL_SUCCEEDED(rc); i++) {
            err_step="SQLSetConnectAttr (from DBD Parameters)";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLSetConnectAttr(hdbc, attrs[i], (void *) attrvals[i], 0);
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        if (connect) {
            SQLCHAR out[1024];
            SQLSMALLINT outlen;
            err_step="SQLDriverConnect";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLDriverConnect(hdbc, NULL, datasource,
                        (SQLSMALLINT) strlen(datasource),
                        out, sizeof(out), &outlen, SQL_DRIVER_NOPROMPT);
        }
        else {
            err_step="SQLConnect";
            err_htype = SQL_HANDLE_DBC;
            err_h = hdbc;
            rc = SQLConnect(hdbc, datasource, (SQLSMALLINT) strlen(datasource),
                        user, (SQLSMALLINT) strlen(user),
                        password, (SQLSMALLINT) strlen(password));
        }
    }
    if (SQL_SUCCEEDED(rc)) {
        handle = apr_pcalloc(pool, sizeof(apr_dbd_t));
        handle->dbname = apr_pstrdup(pool, datasource);
        handle->dbc = hdbc;
        handle->pool = pool;
        handle->defaultBufferSize = defaultBufferSize;
        handle->results = NULL;
        CHECK_ERROR(handle, "SQLConnect", rc, SQL_HANDLE_DBC, handle->dbc);
        handle->default_transaction_mode = 0;
        SQLGetInfo(hdbc, SQL_DEFAULT_TXN_ISOLATION,
                   &(handle->default_transaction_mode), sizeof(int), NULL);
        handle->transaction_mode = handle->default_transaction_mode;
        SQLGetInfo(hdbc, SQL_GETDATA_EXTENSIONS ,&(handle->dboptions),
                   sizeof(int), NULL);
        apr_pool_cleanup_register(pool, handle, odbc_close_cleanup, odbc_close_cleanup);
        return handle;
    }
    else {
        apr_status_t r;
        /* no db handle created, so put error in thread local */
        struct apr_dbd_t *tmp_dbc ;

        r = apr_threadkey_private_get((void **) &tmp_dbc, odbc_tlskey);
        if (r || (tmp_dbc == NULL)) {
            apr_threadkey_private_create(&odbc_tlskey, NULL, pool);
            tmp_dbc = apr_palloc(pool, sizeof(apr_dbd_t));
            r = apr_threadkey_private_set(tmp_dbc, odbc_tlskey);
        }
        tmp_dbc->pool = pool;
        CHECK_ERROR(tmp_dbc, err_step, rc, err_htype, err_h);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        return NULL;
    }
}

/** check_conn: check status of a database connection **/
static apr_status_t odbc_check_conn(apr_pool_t *pool, apr_dbd_t *handle)
{
    SQLUINTEGER isDead;
    SQLRETURN   rc;

    rc = SQLGetConnectAttr(handle->dbc, SQL_ATTR_CONNECTION_DEAD, &isDead,
                            sizeof(SQLUINTEGER), NULL);
    CHECK_ERROR(handle, "SQLGetConnectAttr (SQL_ATTR_CONNECTION_DEAD)", rc,
                SQL_HANDLE_DBC, handle->dbc);
    /* if driver cannot check connection, presume we are still connected */
    if (!SQL_SUCCEEDED(rc))
        return APR_SUCCESS;

    return (isDead == SQL_CD_FALSE) ? APR_SUCCESS : APR_EGENERAL;
}


/** set_dbname: select database name.  May be a no-op if not supported. **/
static int odbc_set_dbname(apr_pool_t* pool, apr_dbd_t *handle,
                           const char *name)
{
    if (apr_strnatcmp(name, handle->dbname)) {
        return APR_EGENERAL;        /* It's illegal to change dbname in ODBC */
    }
    CHECK_ERROR(handle, "set_dbname (no-op)", SQL_SUCCESS, SQL_HANDLE_DBC,
                handle->dbc);
    return APR_SUCCESS;             /* OK if it's the same name */
}

/** transaction: start a transaction.  May be a no-op. **/
static int odbc_start_transaction(apr_pool_t *pool, apr_dbd_t *handle,
                                  apr_dbd_transaction_t **trans)
{
    SQLRETURN rc = SQL_SUCCESS;

    if (handle->transaction_mode) {
        rc = SQLSetConnectAttr(handle->dbc, SQL_ATTR_TXN_ISOLATION, (void *)
                               handle->transaction_mode, 0);
        CHECK_ERROR(handle, "SQLSetConnectAttr (SQL_ATTR_TXN_ISOLATION)", rc,
                    SQL_HANDLE_DBC, handle->dbc);
    }
    if SQL_SUCCEEDED(rc) {
        /* turn off autocommit for transactions */
        rc = SQLSetConnectAttr(handle->dbc, SQL_ATTR_AUTOCOMMIT,
                               SQL_AUTOCOMMIT_OFF, 0);
        CHECK_ERROR(handle, "SQLSetConnectAttr (SQL_ATTR_AUTOCOMMIT)", rc,
                    SQL_HANDLE_DBC, handle->dbc);
    }
    if SQL_SUCCEEDED(rc) {
        *trans = apr_palloc(pool, sizeof(apr_dbd_transaction_t));
        (*trans)->dbc = handle->dbc;
        (*trans)->apr_dbd = handle;
        handle->can_commit = 1;
    }
    return APR_FROM_SQL_RESULT(rc);
};


/** end_transaction: end a transaction **/
static int odbc_end_transaction(apr_dbd_transaction_t *trans)
{
    SQLRETURN rc;
    int action = trans->apr_dbd->can_commit ? SQL_COMMIT : SQL_ROLLBACK;

    rc = SQLEndTran(SQL_HANDLE_DBC, trans->dbc, SQL_COMMIT);
    CHECK_ERROR(trans->apr_dbd, "SQLEndTran", rc, SQL_HANDLE_DBC, trans->dbc);
    if SQL_SUCCEEDED(rc) {
        rc = SQLSetConnectAttr(trans->dbc, SQL_ATTR_AUTOCOMMIT,
                               (SQLPOINTER) SQL_AUTOCOMMIT_ON, 0);
        CHECK_ERROR(trans->apr_dbd, "SQLSetConnectAttr (SQL_ATTR_AUTOCOMMIT)",
                    rc, SQL_HANDLE_DBC, trans->dbc);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** query: execute an SQL query that doesn't return a result set **/
static int odbc_query(apr_dbd_t *handle, int *nrows, const char *statement)
{
    SQLRETURN rc;
    SQLHANDLE hstmt;
    size_t len = strlen(statement);

    odbc_close_results(handle);
    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &hstmt);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc, SQL_HANDLE_DBC,
                handle->dbc);
    if SQL_SUCCEEDED(rc) {
        rc = SQLExecDirect(hstmt, (SQLCHAR *) statement, (SQLINTEGER) len);
        CHECK_ERROR(handle, "SQLExecDirect", rc, SQL_HANDLE_STMT, hstmt);
    }
    if SQL_SUCCEEDED(rc) {
        rc = SQLRowCount(hstmt,  (SQLINTEGER *) nrows);
        CHECK_ERROR(handle, "SQLRowCount", rc, SQL_HANDLE_STMT, hstmt);
    }
    rc = SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
    CHECK_ERROR(handle, "SQLFreeHandle (SQL_HANDLE_STMT)", rc,
                SQL_HANDLE_DBC, handle->dbc);
    return APR_FROM_SQL_RESULT(rc);
}

/** select: execute an SQL query that returns a result set **/
static int odbc_select(apr_pool_t *pool, apr_dbd_t *handle,
                       apr_dbd_results_t **res, const char *statement,
                       int random)
{
    SQLRETURN rc;
    SQLHANDLE hstmt;
    size_t len = strlen(statement);

    odbc_close_results(handle);
    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &hstmt);
    apr_pool_cleanup_register(pool, statement, odbc_close_stmt, odbc_close_stmt);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc, SQL_HANDLE_DBC,
                handle->dbc);
    if (random) {
        rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE,
                            (SQLPOINTER) SQL_SCROLLABLE, 0);
        CHECK_ERROR(handle, "SQLSetStmtAttr (SQL_ATTR_CURSOR_SCROLLABLE)", rc,
                    SQL_HANDLE_STMT, hstmt);
    }
    if SQL_SUCCEEDED(rc) {
        rc = SQLExecDirect(hstmt, (SQLCHAR *) statement, (SQLINTEGER) len);
        CHECK_ERROR(handle, "SQLExecDirect", rc, SQL_HANDLE_STMT, hstmt);
    }
    if SQL_SUCCEEDED(rc) {
        rc = odbc_bind_results(handle, hstmt, pool, random, TEXTMODE, res);
        apr_pool_cleanup_register(pool, handle, 
                                  odbc_close_results, odbc_close_results);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** num_cols: get the number of columns in a results set **/
static int odbc_num_cols(apr_dbd_results_t *res)
{
    return res->ncols;
}

/** num_tuples: get the number of rows in a results set **/
static int odbc_num_tuples(apr_dbd_results_t *res)
{
    SQLRETURN rc;
    SQLINTEGER  nrows;

    rc = SQLRowCount(res->stmt, &nrows);
    CHECK_ERROR(res->apr_dbd, "SQLRowCount", rc, SQL_HANDLE_STMT, res->stmt);
    return SQL_SUCCEEDED(rc) ? (int) nrows : -1;
}

/** get_row: get a row from a result set **/
static int odbc_get_row(apr_pool_t *pool, apr_dbd_results_t *res,
                        apr_dbd_row_t **row, int rownum)
{
    SQLRETURN rc;
    char *fetchType;
    int c;

    *row = apr_pcalloc(pool, sizeof(apr_dbd_row_t));
    (*row)->stmt = res->stmt;
    (*row)->dbc = res->dbc;
    (*row)->res = res;
    (*row)->pool = res->pool;
    /* mark all the columns as un-fetched -for LOBs which allow
     * multiple get_row/datum_get calls */
    for (c=0 ; c < res->ncols ; c++) res->all_data_fetched[c] = 0;
    if (res->random && (rownum > 0) ) {
        fetchType = "SQLFetchScroll";
        rc = SQLFetchScroll(res->stmt, SQL_FETCH_ABSOLUTE, rownum);
    }
    else {
        fetchType = "SQLFetch";
        rc = SQLFetch(res->stmt);
    }
    CHECK_ERROR(res->apr_dbd, fetchType, rc, SQL_HANDLE_STMT, res->stmt);
    (*row)->stmt = res->stmt;
    if (!SQL_SUCCEEDED(rc)) {
        /* early close on any error (usually SQL_NO_DATA) if fetching
         * sequentially to release resources ASAP */
        odbc_close_results(res->apr_dbd);
        return -1;
    }
    return 0;
}

/** get_entry: get an entry from a row (string data) **/
static const char* odbc_get_entry(const apr_dbd_row_t *row, int col)
{
    SQLRETURN rc;

    if (row->res->all_data_fetched[col]
        || (row->res->colsizes[col] == SQL_NULL_DATA))
            return NULL;
    else if (row->res->coltypes[col] == SQL_C_CHAR) {
        row->res->all_data_fetched[col] = 1;
        return apr_pstrdup(row->pool, row->res->colptrs[col]);
    }
    else if (row->res->coltypes[col] == SQL_C_DEFAULT) {
        SQLINTEGER actual_length;
        rc = SQLGetData(row->stmt, col+1, SQL_C_DEFAULT,
                        row->res->colptrs[col], row->res->colsizes[col],
                        &actual_length);
        CHECK_ERROR(row->res->apr_dbd, "SQLGetData", rc, SQL_HANDLE_STMT,
                    row->stmt);
        if (rc == SQL_NO_DATA) {
            row->res->all_data_fetched[col] = 1;
            return NULL;
        }
        return apr_pstrdup(row->pool, row->res->colptrs[col]);
    }
    else return (void *) -1;    /* get_row cannot be used here */
}

/** error: get current error message (if any) **/
static const char* odbc_error(apr_dbd_t *handle, int errnum)
{   /* User can call odbc_error with a NULL handle for _init or _open errors,
     * which don't return a handle. The message is available as long as
     * odbc_error is called from the same thread and with the same pool */
    if (!handle)
    {   apr_dbd_t *dbc = NULL;
        if (odbc_tlskey) apr_threadkey_private_get((void **) &dbc, odbc_tlskey);
        return dbc ? dbc->lastError : "[dbd_odbc]No error message available";
    }
    return handle->lastError;
}

/** escape: escape a string so it is safe for use in query/select **/
static const char* odbc_escape(apr_pool_t *pool, const char *string,
                        apr_dbd_t *handle)
{   // TODO - what's this supposed to do?
    return string;
}

/** prepare: prepare a statement **/
static int odbc_prepare(apr_pool_t *pool, apr_dbd_t *handle, const char *query,
            const char *label, apr_dbd_prepared_t **statement)
{
    SQLRETURN rc;
    size_t len = strlen(query);
    char *ptr = (char *) query;
    int inQuotes =0;
    int nargs =0;
    int i;

    odbc_close_results(handle);
    *statement = apr_pcalloc(pool, sizeof(apr_dbd_prepared_t));
    (*statement)->dbc = handle->dbc;
    (*statement)->apr_dbd = handle;
    while (ptr = strpbrk(ptr, "\'?"))
    {   if (*ptr == '\'') inQuotes = ~inQuotes;
        else if (!inQuotes) nargs++;
        ptr++;
    }
    (*statement)->nvals = (*statement)->nargs = nargs;
    (*statement)->types = apr_palloc(pool, nargs*sizeof(int));
    for(i=0 ; i < nargs ; i++) (*statement)->types[i] = SQL_C_CHAR;

    rc = SQLAllocHandle(SQL_HANDLE_STMT, handle->dbc, &((*statement)->stmt) );
    apr_pool_cleanup_register(pool, *statement, 
                              odbc_close_stmt, odbc_close_stmt);
    CHECK_ERROR(handle, "SQLAllocHandle (STMT)", rc, SQL_HANDLE_DBC,
                handle->dbc);
    rc = SQLPrepare((*statement)->stmt, (SQLCHAR *) query, (SQLINTEGER) len);
    CHECK_ERROR(handle, "SQLPrepare", rc, SQL_HANDLE_STMT, (*statement)->stmt);
    return APR_FROM_SQL_RESULT(rc);
}

/** pquery: query using a prepared statement + args **/
static int odbc_pquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                apr_dbd_prepared_t *statement, int nargs, const char **args)
{   SQLRETURN rc=SQL_SUCCESS;
    int i, argp;

    odbc_close_results(handle);
    for (i=argp=0 ; i < nargs && SQL_SUCCEEDED(rc) ; i++) {
        rc = odbc_bind_param(pool, statement, i+1, statement->types[i], &argp,
                             args, TEXTMODE);
    }
    if SQL_SUCCEEDED(rc) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if SQL_SUCCEEDED(rc) {
        rc = SQLRowCount(statement->stmt, (SQLINTEGER *) nrows);
        CHECK_ERROR(handle, "SQLRowCount", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** pvquery: query using a prepared statement + args **/
static int odbc_pvquery(apr_pool_t *pool, apr_dbd_t *handle, int *nrows,
                 apr_dbd_prepared_t *statement, va_list args)
{
    const char **values;
    int i;
    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char*);
    return odbc_pquery(pool, handle, nrows, statement, i, values);
}

/** pselect: select using a prepared statement + args **/
static int odbc_pselect(apr_pool_t *pool, apr_dbd_t *handle,
                        apr_dbd_results_t **res, apr_dbd_prepared_t *statement,
                        int random, int nargs, const char **args)
{
    SQLRETURN rc = SQL_SUCCESS;
    int i, argp;

    odbc_close_results(handle);
    if (random) {
        rc = SQLSetStmtAttr(statement->stmt, SQL_ATTR_CURSOR_SCROLLABLE,
                            (SQLPOINTER) SQL_SCROLLABLE, 0);
        CHECK_ERROR(handle, "SQLSetStmtAttr (SQL_ATTR_CURSOR_SCROLLABLE)", rc,
                    SQL_HANDLE_STMT, statement->stmt);
    }
    if SQL_SUCCEEDED(rc) {
        for (i=argp=0 ; i < nargs && SQL_SUCCEEDED(rc) ; i++)
        {   rc = odbc_bind_param(pool, statement, i+1, statement->types[i],
                                 &argp, args, TEXTMODE);
        }
    }
    if SQL_SUCCEEDED(rc) {
        rc = SQLExecute(statement->stmt);
        CHECK_ERROR(handle, "SQLExecute", rc, SQL_HANDLE_STMT,
                    statement->stmt);
    }
    if SQL_SUCCEEDED(rc) {
        rc = odbc_bind_results(handle, statement->stmt, pool, random,
                                TEXTMODE, res);
    }
    return APR_FROM_SQL_RESULT(rc);
}

/** pvselect: select using a prepared statement + args **/
static int odbc_pvselect(apr_pool_t *pool, apr_dbd_t *handle,
                         apr_dbd_results_t **res,
                         apr_dbd_prepared_t *statement, int random,
                         va_list args)
{
    const char **values;
    int i;
    values = apr_palloc(pool, sizeof(*values) * statement->nvals);
    for (i = 0; i < statement->nvals; i++)
        values[i] = va_arg(args, const char*);
    return odbc_pselect(pool, handle, res, statement, random, i, values);
}


APU_DECLARE_DATA const apr_dbd_driver_t    apr_dbd_odbc_driver = {
    "odbc",
    odbc_init,
    odbc_native_handle,
    odbc_open,
    odbc_check_conn,
    odbc_close,
    odbc_set_dbname,
    odbc_start_transaction,
    odbc_end_transaction,
    odbc_query,
    odbc_select,
    odbc_num_cols,
    odbc_num_tuples,
    odbc_get_row,
    odbc_get_entry,
    odbc_error,
    odbc_escape,
    odbc_prepare,
    odbc_pvquery,
    odbc_pvselect,
    odbc_pquery,
    odbc_pselect,
};

